package com.gregbugaj.nscaffold.util;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

import com.gregbugaj.nscaffold.annotation.RenderingHint;
import com.gregbugaj.nscaffold.annotation.RenderingHints;

public final class ReflectUtil {
	/**
	 * Prevent instantiation of the class
	 */
	private ReflectUtil(){

	}

	/**
	 * Get value of the method
	 * @param value
	 * @param method
	 * @return
	 * @throws Exception
	 */
	public static Object getMethodValue(final Object value, final Method method) throws Exception {
		Object propertyValue = null;				
		try {
			propertyValue = method.invoke(value, new Object[0]);
		} catch (Exception e) {
			throw new Exception("Cannot get field " + method.getDeclaringClass().getName() + "."
					+ method.getName());
		}		
		return propertyValue;
	}

	/**
	 * Get a list of fields that have this annotation including fields declared in any super class
	 */
	public static List<Field> getFieldsWithAnnotation(final Class<?> clazz, final Class<? extends Annotation> annotation) {
		List<Field> fields = new ArrayList<Field>();
		for (Class<?> superClass = clazz; superClass != Object.class; superClass = superClass.getSuperclass()) {
			for (Field field : superClass.getDeclaredFields()) {
				if (field.isAnnotationPresent(annotation)) {
					fields.add(field);
				}
			}
		}
		return fields;
	}


	/**
	 * Get all rendering hint associated with this extensionclass
	 * @param extensionClass
	 * @return
	 */
	public static List<RenderingHint> getRenderingHintAnnotation(final Class<?> extensionClass) {
		Annotation[] annotations = extensionClass.getAnnotations();
		List<RenderingHint> renderingHints = new ArrayList<RenderingHint>();
		for(Annotation an:annotations){
			if(an.annotationType().equals(RenderingHints.class)){				
				RenderingHint[] hints =((RenderingHints) an).value();	
				renderingHints.addAll(Arrays.asList(hints));												
			}
			if(an.annotationType().equals(RenderingHint.class)){				
				renderingHints.add((RenderingHint) an);				
			}
		}
		return renderingHints;
	}
	
	/**
	 * 
	 * @param extensionClass
	 * @param property
	 * @return
	 */
	public static RenderingHint getRenderingHint(Class<?> extensionClass, String property) {
		for(RenderingHint hint : getRenderingHintAnnotation(extensionClass)){
			if(hint.property().equals(property))return hint;
		}
		return null;
	}

	public static boolean hasRenderingHint(Class<?> extensionClass, String property) {
		for(RenderingHint hint : getRenderingHintAnnotation(extensionClass)){
			if(hint.property().equals(property))return true;
		}
		return false;
	}

	/**
	 * Get the first field that has this annotation including fields declared in any super class
	 */
	public static Field getFieldWithAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
		for (Class<?> superClass = clazz; superClass != Object.class; superClass = superClass.getSuperclass()) {
			for (Field field : superClass.getDeclaredFields()) {
				if (field.isAnnotationPresent(annotation)) {
					return field;
				}
			}
		}
		return null;
	}

	/**
	 * Get a list of methods that have this annotation including fields declared in any super class
	 */
	public static List<Method> getMethodsWithAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
		List<Method> methods = new ArrayList<Method>();
		for (Method method : clazz.getMethods()) {
			if (method.isAnnotationPresent(annotation)) {
				methods.add(method);
			}
		}
		return methods;
	}


	/**
	 * Try to make a field accessible 
	 * @param field to make accessible
	 * @return true if the field is accessible or if it have been made accessible
	 */
	public static boolean makeAccessible(Field field) {
		boolean retVal=true;
		if(!field.isAccessible()){
			try{
				field.setAccessible(true);				
			}catch(SecurityException sec){						
				retVal=false;				
			}
		}		
		return retVal;
	}



	/**
	 * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
	 * http://snippets.dzone.com/posts/show/4831
	 * @param packageName The base package
	 * @return The classes
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public static List<Class<?>> getClasses(String packageName)
	throws ClassNotFoundException, IOException 
	{
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		assert classLoader != null;
		String path = packageName.replace('.', '/');
		Enumeration<URL> resources = classLoader.getResources(path);
		List<File> dirs = new ArrayList<File>();
		while (resources.hasMoreElements()) {
			URL resource = resources.nextElement();
			String fileName = resource.getFile();
			String fileNameDecoded = URLDecoder.decode(fileName, "UTF-8");
			dirs.add(new File(fileNameDecoded));
		}
		ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
		for (File directory : dirs) {
			classes.addAll(findClasses(directory, packageName));
		}
		return classes;
	}

	/**
	 * Recursive method used to find all classes in a given directory and subdirs.
	 *
	 * @param directory   The base directory
	 * @param packageName The package name for classes found inside the base directory
	 * @return The classes
	 * @throws ClassNotFoundException
	 */
	@SuppressWarnings("unchecked")
	private static List<Class<?>> findClasses(File directory, String packageName) throws ClassNotFoundException 
	{        
		if (!directory.exists()) {
			return Collections.<Class<?>>emptyList();
		}

		List<Class<?>> classes = new ArrayList<Class<?>>();
		File[] files = directory.listFiles();
		for (File file : files) {
			String fileName = file.getName();
			if (file.isDirectory()) {
				assert !fileName.contains(".");
				classes.addAll(findClasses(file, packageName + "." + fileName));
			} else if (fileName.endsWith(".class") && !fileName.contains("$")) {
				Class _class;
				try {
					_class = Class.forName(packageName + '.' + fileName.substring(0, fileName.length() - 6));
				} catch (ExceptionInInitializerError e) {
					// happen, for example, in classes, which depend on 
					// Spring to inject some beans, and which fail, 
					// if dependency is not fulfilled
					_class = Class.forName(packageName + '.' + fileName.substring(0, fileName.length() - 6),
							false, Thread.currentThread().getContextClassLoader());
				}
				classes.add(_class);
			}
		}
		return classes;
	}


	public static Class<?> getExtensionClass(Class<?> cls, String extensionPackageName) {
		List<Class<?>> classes;
		try {
			classes = getClasses(extensionPackageName);
			for(Class<?> clazz:classes){
				for (Class<?> superClass = clazz; superClass != Object.class; superClass = superClass.getSuperclass()) {
					if(cls==superClass){
						return clazz;
					}
				}
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * Check if give class has annotation present on any of the fields
	 * @param extensionClass
	 * @param annotationClass
	 * @return
	 */
	public static boolean hasMethodAnnotation(Class<?> clazz, Class<? extends Annotation> annotationClass) {
		for (Class<?> superClass = clazz; superClass != Object.class && superClass != null; superClass = superClass.getSuperclass()) {
			Method[] methods = superClass.getMethods();
			for(Method method:methods){
				if(method.isAnnotationPresent(annotationClass))return true;
			}
		}
		return false;
	}

	
	/**
	 * Check if give class has annotation present on any of the fields
	 * @param extensionClass
	 * @param annotationClass
	 * @return
	 */
	public static Method getAnnotatedMethod(Class<?> clazz, Class<? extends Annotation> annotationClass) {
		for (Class<?> superClass = clazz; superClass != Object.class; superClass = superClass.getSuperclass()) {
			Method[] methods = superClass.getMethods();
			for(Method method:methods){
				Annotation anotation = method.getAnnotation(annotationClass);
				if(anotation!=null)return method;
			}
		}
		return null;
	}

	/**
	 * Convert method to bean name specification
	 * @param method
	 * @return
	 */
	public static String getBeanName(Method method) {
		if (method==null) throw new IllegalArgumentException("Method can't be null");		
		String name=method.getName();
		name=name.replace("get", "");		
		return name.toLowerCase();
	
	}


	public static Object getFieldValue(Object value, Field field) {
		Object propertyValue = null;
		boolean accessible = field.isAccessible();
		field.setAccessible(true);
		try {
			propertyValue = field.get(value);
		} catch (Exception e) {
			throw new RuntimeException("Cannot get field " + field.getDeclaringClass().getName() + "." + field.getName()); //$NON-NLS-1$ //$NON-NLS-2$
		} finally {
			field.setAccessible(accessible);
		}
		return propertyValue;
	}



}

